﻿using System.Collections.Generic;
using System.Linq;

using Styx;
using Styx.Logic;
using Styx.Logic.Pathing;
using Styx.WoWInternals;
using Styx.WoWInternals.WoWObjects;

namespace SPR.Helpers
{
    using Styx.Combat.CombatRoutine;
    using Styx.Logic.Combat;

    public class Unit
    {
        /* putting all the Unit logic here */

        private static readonly Unit UnitInstance = new Unit();

        /// <summary>
        /// An instance of the Unit Class
        /// </summary>
        public static Unit Instance { get { return UnitInstance; } }

        private static LocalPlayer Me
        {
            get { return StyxWoW.Me; }
        }

        /// <summary>
        /// Crowd controlled
        /// </summary>
        /// <param name="unit">unit to check for</param>
        /// <returns>true if controlled</returns>
        public static bool IsCrowdControlled(WoWUnit unit)
        {
            Dictionary<string, WoWAura>.ValueCollection auras = unit.Auras.Values;

            return auras.Any(
                a => a.Spell.Mechanic == WoWSpellMechanic.Banished ||
                     a.Spell.Mechanic == WoWSpellMechanic.Charmed ||
                     a.Spell.Mechanic == WoWSpellMechanic.Horrified ||
                     a.Spell.Mechanic == WoWSpellMechanic.Incapacitated ||
                     a.Spell.Mechanic == WoWSpellMechanic.Polymorphed ||
                     a.Spell.Mechanic == WoWSpellMechanic.Sapped ||
                     a.Spell.Mechanic == WoWSpellMechanic.Shackled ||
                     a.Spell.Mechanic == WoWSpellMechanic.Asleep ||
                     a.Spell.Mechanic == WoWSpellMechanic.Frozen ||
                     a.Spell.Mechanic == WoWSpellMechanic.Invulnerable ||
                     a.Spell.Mechanic == WoWSpellMechanic.Invulnerable2 ||
                     a.Spell.Mechanic == WoWSpellMechanic.Turned ||
                     a.Spell.Mechanic == WoWSpellMechanic.Fleeing ||

                     // Really want to ignore hexed mobs.
                     a.Spell.Name == "Hex");
        }

        /// <summary>
        ///     List of nearby Ranged enemy units that pass certain criteria, this list should only return units 
        ///     in active combat with the player, the player's party, or the player's raid.
        /// </summary>
        public static IEnumerable<WoWUnit> RangedEnemyUnits
        {
            get
            {
                return
                    ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
                        .Where(unit =>
                               !unit.IsFriendly
                               && (unit.IsTargetingMeOrPet
                                   || unit.IsTargetingMyPartyMember
                                   || unit.IsTargetingMyRaidMember
                                   || unit.IsPlayer
                                   || unit.MaxHealth == 1)
                               && !unit.IsNonCombatPet
                               && !unit.IsCritter
                               && unit.Distance2D
                               <= 40).ToList();
            }
        }

        /// <summary>
        ///     List of nearby enemy units that pass certain criteria, this list should only return units 
        ///     in active combat with the player, the player's party, or the player's raid.
        /// </summary>
        public static IEnumerable<WoWUnit> EnemyUnits
        {
            get
            {
                return
                    ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
                        .Where(unit =>
                               !unit.IsFriendly
                               && (unit.IsTargetingMeOrPet
                                   || unit.IsTargetingMyPartyMember
                                   || unit.IsTargetingMyRaidMember
                                   || unit.IsPlayer
                                   || unit.MaxHealth == 1)
                               && !unit.IsNonCombatPet
                               && !unit.IsCritter
                               && unit.Distance2D
                               <= 12).ToList();
            }
        }

        /// <summary>
        /// checks if targe will die in the next 15 sec.
        /// Assumming if in raid everyone is doing 15k dps
        /// and 10k dps if in party
        /// </summary>
        /// <param name = "target">the target to check</param>
        /// <returns>true or false</returns>
        public bool IsTargetLivingLong(WoWUnit target)
        {
            int damage;
            if (Me.IsInRaid)
            {
                damage = Me.RaidMembers.Count * 15000 * 15;
            }
            else if (Me.IsInParty)
            {
                damage = Me.PartyMembers.Count * 15000 * 15;
            }
            else
            {
                damage = 20000 * 15;
            }
            return target.CurrentHealth > damage;
        }

        /// <summary>
        /// checks if target is worth blowing a cooldown on
        /// </summary>
        /// <param name="target">the target to check</param>
        /// <returns>true or false</returns>
        public bool IsTargetWorthy(WoWUnit target)
        {
            if (!SPR.Instance.AutoManageCooldowns)
                return false;

            if (target == null)
                return false;

            var trainingDummy = Lists.BossList.TrainingDummies.Contains(target.Entry);

            if (!IsTargetLivingLong(target) && !trainingDummy)
                return false;

            var boss = Lists.BossList.BossIds.Contains(target.Entry);
            var miniBoss = (target.Level >= Me.Level + 2) && target.Elite;

            var targetIsWorthy = boss || miniBoss || trainingDummy;
            if (targetIsWorthy)
            {
                SPR.DebugLog(
                    string.Format(
                        "[IsTargetWorthy] {0} is a boss? {1} or miniBoss? {2} or Training Dummy? {4}. {0} current Health = {3}",
                        SPR.SafeName(target),
                        boss,
                        miniBoss,
                        target.CurrentHealth,
                        trainingDummy));
            }

            return targetIsWorthy;
        }

        /// <summary>
        /// Checks to see if we are silenced or stunned (used for cancast)
        /// </summary>
        /// <param name="unit">the unit to check</param>
        /// <returns>returns is incapacitated status</returns>
        public bool IsIncapacitated(WoWUnit unit)
        {
            var restrained = unit.Stunned || unit.Silenced || unit.Rooted;
            if (restrained)
            {
                SPR.DebugLog(
                    string.Format(
                        "[IsIncapacitated] how am I restrained? stunned: {0} Silenced: {1}", unit.Stunned, unit.Silenced));
            }

            return restrained;
        }

        /// <summary>
        /// 	Check for players to resurrect
        /// </summary>
        public List<WoWPlayer> ResurrectablePlayers
        {
            get
            {
                return ObjectManager.GetObjectsOfType<WoWPlayer>().Where(
                    p => !p.IsMe && p.Dead && p.IsFriendly && p.IsInMyPartyOrRaid && p.DistanceSqr < 30 * 30).ToList();
            }
        }

        /// <summary>
        /// Returns a list of tanks
        /// </summary>
        private static IEnumerable<WoWPlayer> Tanks
        {
            get
            {
                var result = new List<WoWPlayer>();

                if (!StyxWoW.Me.IsInParty)
                    return result;

                if ((StyxWoW.Me.Role & WoWPartyMember.GroupRole.Tank) != 0)
                    result.Add(StyxWoW.Me);

                var members = StyxWoW.Me.IsInRaid ? StyxWoW.Me.RaidMemberInfos : StyxWoW.Me.PartyMemberInfos;

                var tanks = members.Where(p => (p.Role & WoWPartyMember.GroupRole.Tank) != 0);

                result.AddRange(tanks.Where(t => t.ToPlayer() != null).Select(t => t.ToPlayer()));

                return result;
            }
        }

        /// <summary>
        /// returns a list of healers.
        /// </summary>
        public static List<WoWPlayer> Healers
        {
            get
            {
                var result = new List<WoWPlayer>();

                if (!StyxWoW.Me.IsInParty)
                    return result;

                if ((StyxWoW.Me.Role & WoWPartyMember.GroupRole.Healer) != 0)
                    result.Add(StyxWoW.Me);

                var members = StyxWoW.Me.IsInRaid ? StyxWoW.Me.RaidMemberInfos : StyxWoW.Me.PartyMemberInfos;

                var tanks = members.Where(p => (p.Role & WoWPartyMember.GroupRole.Healer) != 0);

                result.AddRange(tanks.Where(t => t.ToPlayer() != null).Select(t => t.ToPlayer()));

                return result;
            }
        }

        public WoWUnit BestTricksTarget
        {
            get
            {
                if (!StyxWoW.Me.IsInParty && !StyxWoW.Me.IsInRaid)
                    return null;

                // If the player has a focus target set, use it instead. TODO: Add Me.FocusedUnit to the HB API.
                if (StyxWoW.Me.FocusedUnitGuid != 0)
                    return StyxWoW.Me.FocusedUnit;

                if (StyxWoW.Me.IsInInstance)
                {
                    if (RaFHelper.Leader != null && !RaFHelper.Leader.IsMe)
                    {
                        // Leader first, always. Otherwise, pick a rogue/DK/War pref. Fall back to others just in case.
                        return RaFHelper.Leader;
                    }

                    if (StyxWoW.Me.IsInParty)
                    {
                        var bestTank = Tanks.OrderBy(t => t.DistanceSqr).FirstOrDefault(t => t.IsAlive);

                        if (bestTank != null)
                            return bestTank;
                    }

                    var bestPlayer = GetPlayerByClassPrio(
                        100f,
                        false,
                        WoWClass.Rogue,
                        WoWClass.DeathKnight,
                        WoWClass.Warrior,
                        WoWClass.Hunter,
                        WoWClass.Mage,
                        WoWClass.Warlock,
                        WoWClass.Shaman,
                        WoWClass.Druid,
                        WoWClass.Paladin,
                        WoWClass.Priest);
                    return bestPlayer;
                }

                return null;
            }
        }

        /// <summary>Gets a player by class priority. The order of which classes are passed in, is the priority to find them.</summary>
        /// <remarks>Created 9/9/2011.</remarks>
        /// <param name="range">distance to player</param>
        /// <param name="includeDead">true or false</param>
        /// <param name="classes">A variable-length parameters list containing classes.</param>
        /// <returns>The player by class prio.</returns>
        private static WoWUnit GetPlayerByClassPrio(float range, bool includeDead, params WoWClass[] classes)
        {
            return (from woWClass in classes select StyxWoW.Me.PartyMemberInfos.FirstOrDefault(p => p.ToPlayer() != null && p.ToPlayer().Distance < range && p.ToPlayer().Class == woWClass) into unit where unit != null where !includeDead && unit.Dead || unit.Ghost select unit.ToPlayer()).FirstOrDefault();
        }

        /// <summary>Locates nearby units from location</summary>
        /// <param name="fromLocation">units location</param>
        /// <param name="radius">radius</param>
        /// <param name="playersOnly">true for players only</param>
        /// <returns>The nearby units.</returns>
        private List<WoWUnit> NearbyUnits(WoWPoint fromLocation, double radius, bool playersOnly)
        {
            List<WoWUnit> hostile = ObjectManager.GetObjectsOfType<WoWUnit>(true, false);
            var maxDistance2 = radius * radius;

            if (playersOnly)
            {
                hostile = hostile.Where(x =>
                                        x.IsPlayer && IsAttackable(x)
                                        && x.Location.Distance2DSqr(fromLocation) < maxDistance2).ToList();
            }
            else
            {
                hostile = hostile.Where(x =>
                                        !x.IsPlayer && IsAttackable(x)
                                        && x.Location.Distance2DSqr(fromLocation) < maxDistance2).ToList();
            }

            SPR.DebugLog("CountEnnemiesInRange");
            foreach (var u in hostile)
                SPR.DebugLog(" -> " + SPR.SafeName(u) + " " + u.Level);
            SPR.DebugLog("---------------------");
            return hostile;
        }

        /// <summary>returns the amount of targets from the units location</summary>
        /// <param name="fromLocation">units location</param>
        /// <param name="maxRange">maximum range</param>
        /// <returns>The count ennemies in range.</returns>
        public int CountEnnemiesInRange(WoWPoint fromLocation, double maxRange)
        {
            return NearbyUnits(fromLocation, maxRange, Battlegrounds.IsInsideBattleground).Count;
        }

        /// <summary>Finds clustered targets</summary>
        /// <param name="radius">radius</param>
        /// <param name="minDistance">minimum distance</param>
        /// <param name="maxDistance">maximum distance</param>
        /// <param name="minTargets">minimum targets to qualify</param>
        /// <param name="playersOnly">true for players only</param>
        /// <returns>The find cluster targets.</returns>
        public WoWPoint FindClusterTargets(double radius, double minDistance, double maxDistance, int minTargets, bool playersOnly)
        {
            List<WoWUnit> hostile = ObjectManager.GetObjectsOfType<WoWUnit>(true, false);
            var avoid = new List<WoWUnit>();
            var maxDistance2 = (maxDistance + radius) * (maxDistance + radius);

            if (playersOnly)
            {
                hostile = hostile.Where(x =>
                    x.IsPlayer &&
                    IsAttackable(x) && x.Distance2DSqr < maxDistance2).ToList();
            }
            else
            {
                hostile = hostile.Where(x =>
                    !x.IsPlayer &&
                    IsAttackable(x) && x.Distance2DSqr < maxDistance2).ToList();
                avoid = hostile.Where(
                    x => // check for controlled units, like sheep etc
                    UnitIsControlled(x, true)).ToList();
            }

            if (hostile.Count < minTargets)
            {
                return WoWPoint.Empty;
            }

            var score = minTargets - 1;
            var best = WoWPoint.Empty;

            for (var x = Me.Location.X - maxDistance; x <= Me.Location.X + maxDistance; x++)
            {
                for (var y = Me.Location.Y - maxDistance; y <= Me.Location.Y + maxDistance; y++)
                {
                    var spot = new WoWPoint(x, y, Me.Location.Z);
                    var dSquare = spot.Distance2DSqr(Me.Location);
                    if (dSquare > maxDistance * maxDistance || dSquare < minDistance * minDistance)
                    {
                        continue;
                    }

                    if (avoid.Any(t => t.Location.Distance2DSqr(spot) <= radius * radius))
                    {
                        continue;
                    }

                    var hits = hostile.Count(t => t.Location.DistanceSqr(spot) < radius * radius);
                    if (hits > score)
                    {
                        best = spot;
                        score = hits;
                        SPR.DebugLog("ClusteredTargets(range=" + minDistance + "-" + maxDistance + ", radius=" + radius + ") => SCORE=" + score + " at " + spot);
                        foreach (var u in hostile.Where(t => t.Location.DistanceSqr(spot) < radius * radius))
                            SPR.DebugLog(" -> " + SPR.SafeName(u) + " " + u.Level);
                        SPR.DebugLog("---------------------");
                    }
                }
            }

            return best;
        }

        /// <summary>Returns true if the unit is attackable</summary>
        /// <param name="unit">unit to check for</param>
        /// <returns>The is attackable.</returns>
        private static bool IsAttackable(WoWUnit unit)
        {
            return
                unit.IsValid &&
                unit.IsAlive &&
                unit.CanSelect &&
                // never focus a player pet - mobs pets are fine
                (unit.OwnedByUnit == null || !unit.OwnedByUnit.IsPlayer) &&
                (unit.CreatedByUnit == null || !unit.CreatedByUnit.IsPlayer) &&
                // other stuff
                !unit.IsNonCombatPet &&
                !unit.IsFriendly;
        }

        private readonly string[] controlDebuffs = new[]{
            "Bind Elemental",
            "Hex",
            "Polymorph",
            "Hibernate",
            "Entangling Roots",
            "Freezing Trap",
            "Wyvern Sting",
            "Repentance",
            "Psychic Scream",
            "Sap",
            "Blind",
            "Fear",
            "Seduction",
            "Howl of Terror"
        };
        private readonly string[] controlUnbreakableDebuffs = new[]{
            "Cyclone",
            "Mind Control",
            "Banish"
        };

        /// <summary>returns true if the unit is crowd controlled.</summary>
        /// <param name="unit">unit to check</param>
        /// <param name="breakOnDamageOnly">true for break on damage</param>
        /// <returns>The unit is controlled.</returns>
        private bool UnitIsControlled(WoWUnit unit, bool breakOnDamageOnly)
        {
            return unit.ActiveAuras.Any(x => x.Value.IsHarmful &&
                                             (this.controlDebuffs.Contains(x.Value.Name) ||
                                              (!breakOnDamageOnly && this.controlUnbreakableDebuffs.Contains(x.Value.Name))));
        }

        /// <summary>
        /// Returns true if Morchok is casting Stomp
        /// </summary>
        /// <returns>true or false</returns>
        public bool IsMorchokStomp()
        {
            return
                ObjectManager.GetObjectsOfType<WoWUnit>(true, false).Any(
                    u =>
                    u.IsAlive && u.Guid != Me.Guid && u.IsHostile && u.IsCasting
                    && u.CastingSpell.Name == "Stomp" && u.Name == "Morchok");
        }

        /// <summary>
        /// Returns true if Ultraxion is casting Hour of Twilight
        /// </summary>
        /// <returns>true or false</returns>
        public bool IsHourofTwilight()
        {
            return
                ObjectManager.GetObjectsOfType<WoWUnit>(true, true).Any(
                    u =>
                    u.IsAlive && u.Guid != Me.Guid && u.IsHostile && u.IsCasting
                    && u.CastingSpell.Name == "Hour of Twilight" && u.CurrentCastTimeLeft.TotalMilliseconds <= 800);
        }
        
        /// <summary>
        /// Returns true if Ultraxion is casting Fading Light
        /// </summary>
        /// <returns>true or false</returns>
        public bool IsFadingLight()
        {
            return Buff.PlayerHasActiveBuff("Fading Light")
                   && Buff.PlayerDebuffTimeLeft("Fading Light").TotalSeconds <= 2;
        }

        ///// <summary>
        ///// Returns true if Ultraxion is casting Fading Light
        ///// </summary>
        ///// <returns>true or false</returns>
        // public bool IsFadingLight()
        // {
        //    return
        //        ObjectManager.GetObjectsOfType<LocalPlayer>(true, true).Cast<WoWUnit>().Any(
        //            u =>
        //            u.Debuffs.ContainsKey("Fading Light") && u.Debuffs["Fading Light"].IsActive
        //            && u.Debuffs["Fading Light"].TimeLeft.TotalMilliseconds <= 2000);
        // }

        /// <summary>
        /// Returns true if DeathWing is casting Shrapnel
        /// </summary>
        /// <returns>true or false</returns>
        public bool IsShrapnel()
        {
            return
                ObjectManager.GetObjectsOfType<WoWUnit>(true, true).Any(
                    u =>
                    u.IsAlive && u.Guid != Me.Guid && u.IsHostile
                    &&
                    (u.IsTargetingMyPartyMember || u.IsTargetingMyRaidMember || u.IsTargetingMeOrPet
                     || u.IsTargetingAnyMinion) && u.IsCasting && u.CastingSpell.Name == "Shrapnel"
                    && u.CurrentCastTimeLeft.TotalMilliseconds <= 2000);
        }

        public static Dictionary<string, int> needsSlow {
            get {
                Dictionary<string, int> dictionary = new Dictionary<string, int>();
                dictionary.Add("Twilight Sapper", 1);
                return dictionary;
            }
        }
    }
}
